fix: address Copilot review comments from PR #22#23
Conversation
- Extract shared helpers (isCLIError, buildRepoMapAttachment, resolveWikiDir, inferLangFromFile, filterWorkspaceRowsByLang) into sharedHelpers.ts to eliminate duplication between queryHandlers.ts and queryFilesHandlers.ts - Add path traversal protection in resolveWikiDir: reject paths that escape repoRoot - Fix wildcard SQL prefilter: convert glob patterns to SQL LIKE patterns (globToSqlLike) instead of treating raw glob as substring - Return explicit error for workspace mode in query-files since queryManifestWorkspace queries by symbol, not file name - Remove unused import (resolveLangs) from queryFilesHandlers.ts - Convert Commander.js string options to numbers in queryFilesCommand.ts (limit, maxCandidates, repoMapFiles, repoMapSymbols) - Improve tests: add proper assertions for wildcard, language filtering, empty pattern (via Zod schema), and invalid mode (via Zod schema) - Remove unused variable in empty pattern test
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
mars167
left a comment
There was a problem hiding this comment.
Review completed by CodaGraph AI Agent.
Additional Comments
The following comments could not be placed inline:
src/cli/handlers/queryHandlers.ts (Line L10-L14)
代码将 isCLIError、buildRepoMapAttachment、filterWorkspaceRowsByLang 三个函数从当前文件删除,并改为从 './sharedHelpers' 导入。但 diff 未显示 sharedHelpers 文件的变更,需确认该文件存在且正确导出这些函数,否则会导致模块加载失败
建议: 确认 ./sharedHelpers.ts 文件已创建并正确导出三个函数:export { isCLIError, buildRepoMapAttachment, filterWorkspaceRowsByLang }
// sharedHelpers.ts 应包含:
export { isCLIError } from './sharedHelpers';
export { buildRepoMapAttachment } from './sharedHelpers';
export { filterWorkspaceRowsByLang } from './sharedHelpers';
// 或
import { isCLIError, buildRepoMapAttachment, filterWorkspaceRowsByLang } from './sharedHelpers';
src/cli/handlers/queryHandlers.ts (Line L2-L6, DEL)
💡 SUGGESTION: 被移除的导入语句可能残留未清理
删除了 import { checkIndex, resolveLangs } from '../../core/indexCheck' 和 import { generateRepoMap, type FileRank } from '../../core/repoMap',需确认这些导入在其他文件中不再需要,避免遗留死代码
建议: 全局搜索 checkIndex、resolveLangs、generateRepoMap、FileRank 确保无其他引用
| .option('--wiki <dir>', 'Wiki directory (default: docs/wiki or wiki)', '') | ||
| .action(async (pattern, options) => { | ||
| await executeHandler('query-files', { pattern, ...options }); | ||
| const limit = parseInt(options.limit, 10); |
There was a problem hiding this comment.
parseInt 在处理非数字字符串时返回 NaN(如 --limit abc),后续 executeHandler 收到 NaN 可能导致静默错误或意外行为
建议: 添加验证逻辑,过滤无效值:
const limit = /^-?\d+$/.test(options.limit) ? parseInt(options.limit, 10) : undefined;
或使用默认值兜底
| const limit = parseInt(options.limit, 10); | |
| const limit = parseInt(options.limit, 10); | |
| if (isNaN(limit)) { | |
| console.error(`Invalid limit: ${options.limit}`); | |
| return; | |
| } |
| .option('--wiki <dir>', 'Wiki directory (default: docs/wiki or wiki)', '') | ||
| .action(async (pattern, options) => { | ||
| await executeHandler('query-files', { pattern, ...options }); | ||
| const limit = parseInt(options.limit, 10); |
There was a problem hiding this comment.
💡 SUGGESTION: 缺乏参数验证提示
CLI 参数错误时未向用户输出友好的错误信息,用户无法得知应如何修正
建议: 捕获无效输入并输出用法提示,帮助用户正确使用命令
| wikiDir, | ||
| }); | ||
| return { enabled: true, wikiDir, files }; | ||
| } catch (e: any) { |
There was a problem hiding this comment.
catch 块中使用 e: any 掩盖了错误的类型信息,不利于调试和错误追踪
建议: 使用 unknown 替代 any,并通过类型守卫安全访问错误信息
| } catch (e: any) { | |
| } catch (e: unknown) { | |
| const message = e instanceof Error ? e.message : String(e); | |
| return { enabled: false, skippedReason: message }; | |
| } |
| if (w) { | ||
| const resolved = path.resolve(repoRoot, w); | ||
| // Prevent path traversal outside repoRoot | ||
| if (!resolved.startsWith(repoRoot + path.sep) && resolved !== repoRoot) { |
There was a problem hiding this comment.
使用 path.sep 进行路径比较在 Windows (\) 和 Unix (/) 混合环境中可能失效。推荐使用 path.relative() 进行更健壮的路径校验
建议: 改用 path.relative 进行安全校验
| if (!resolved.startsWith(repoRoot + path.sep) && resolved !== repoRoot) { | |
| const relative = path.relative(repoRoot, resolved); | |
| if (relative.startsWith('..') || path.isAbsolute(relative)) { | |
| return ''; | |
| } |
| return 'ts'; | ||
| } | ||
|
|
||
| export function filterWorkspaceRowsByLang(rows: any[], langSel: string): any[] { |
There was a problem hiding this comment.
💡 SUGGESTION: 函数参数和返回值缺少类型定义
函数签名使用 any[] 类型,丢失了 TypeScript 的类型安全优势
建议: 定义明确的接口类型来描述 rows 的结构
| export function filterWorkspaceRowsByLang(rows: any[], langSel: string): any[] { | |
| interface WorkspaceRow { | |
| file: string; | |
| // 其他字段... | |
| } | |
| export function filterWorkspaceRowsByLang(rows: WorkspaceRow[], langSel: string): WorkspaceRow[] { |
| import type { CLIError } from '../types'; | ||
|
|
||
| export function isCLIError(value: unknown): value is CLIError { | ||
| return typeof value === 'object' && value !== null && 'ok' in value && (value as any).ok === false; |
There was a problem hiding this comment.
📝 NIT: 类型守卫可进一步优化
使用 (value as any).ok 绕过了类型检查,略显冗余
建议: 使用可选链和更严格的类型检查
| return typeof value === 'object' && value !== null && 'ok' in value && (value as any).ok === false; | |
| return ( | |
| typeof value === 'object' && | |
| value !== null && | |
| 'ok' in value && | |
| (value as CLIError).ok === false | |
| ); |
There was a problem hiding this comment.
Pull request overview
This PR addresses prior review feedback for the query-files CLI command by hardening wiki path handling, fixing wildcard SQL prefiltering, clarifying unsupported workspace behavior, and reducing duplicated handler utilities.
Changes:
- Adds shared handler utilities (
sharedHelpers.ts) and updatesquery/query-fileshandlers to use them. - Fixes wildcard SQL prefiltering by converting glob patterns to SQL
LIKEpatterns. - Improves
query-filesbehavior/tests (schema-level validation tests, stronger wildcard/lang assertions) and parses numeric CLI options before handler execution.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/queryFiles.test.ts | Strengthens assertions and shifts some invalid-input checks to Zod schema validation. |
| src/cli/handlers/sharedHelpers.ts | Introduces shared helpers incl. resolveWikiDir containment check and repo-map attachment builder. |
| src/cli/handlers/queryHandlers.ts | Removes duplicated helpers and uses shared helper implementations. |
| src/cli/handlers/queryFilesHandlers.ts | Adds glob→SQL LIKE conversion and changes workspace mode to an explicit error path. |
| src/cli/commands/queryFilesCommand.ts | Parses numeric CLI options prior to calling executeHandler. |
| package-lock.json | Version bump / lockfile metadata updates. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /** | ||
| * Resolve wiki directory, ensuring the resolved path stays within repoRoot | ||
| * to prevent path traversal attacks. | ||
| */ | ||
| export function resolveWikiDir(repoRoot: string, wikiOpt: string): string { |
There was a problem hiding this comment.
The PR description says wiki path traversal is addressed, but only query/query-files use the new sharedHelpers.resolveWikiDir. repo-map and semantic handlers still have their own resolveWikiDir implementations without the containment check (see src/cli/handlers/repoMapHandler.ts and src/cli/handlers/semanticHandlers.ts). Consider switching those handlers to import/use resolveWikiDir from sharedHelpers as well so the protection is consistent across commands.
| const limit = parseInt(options.limit, 10); | ||
| const maxCandidates = parseInt(options.maxCandidates, 10); | ||
| const repoMapFiles = parseInt(options.repoMapFiles, 10); | ||
| const repoMapSymbols = parseInt(options.repoMapSymbols, 10); | ||
| await executeHandler('query-files', { | ||
| pattern, | ||
| ...options, | ||
| limit, | ||
| maxCandidates, | ||
| repoMapFiles, | ||
| repoMapSymbols, |
There was a problem hiding this comment.
Parsing numeric options with parseInt here weakens validation compared to the Zod schema coercion: values like --limit 10abc will be accepted as 10 instead of rejected. Either pass the raw string values through to executeHandler and let SearchFilesSchema coerce/validate them, or explicitly validate that the entire string is a base-10 integer (and error on NaN / trailing junk).
| const limit = parseInt(options.limit, 10); | |
| const maxCandidates = parseInt(options.maxCandidates, 10); | |
| const repoMapFiles = parseInt(options.repoMapFiles, 10); | |
| const repoMapSymbols = parseInt(options.repoMapSymbols, 10); | |
| await executeHandler('query-files', { | |
| pattern, | |
| ...options, | |
| limit, | |
| maxCandidates, | |
| repoMapFiles, | |
| repoMapSymbols, | |
| await executeHandler('query-files', { | |
| pattern, | |
| ...options, |
| import { | ||
| isCLIError, | ||
| buildRepoMapAttachment, | ||
| filterWorkspaceRowsByLang, |
There was a problem hiding this comment.
filterWorkspaceRowsByLang is imported but no longer used after workspace mode was changed to return an error. Please remove the unused import to keep the module tidy.
| filterWorkspaceRowsByLang, |
| * Resolve wiki directory, ensuring the resolved path stays within repoRoot | ||
| * to prevent path traversal attacks. | ||
| */ | ||
| export function resolveWikiDir(repoRoot: string, wikiOpt: string): string { | ||
| const w = String(wikiOpt ?? '').trim(); | ||
| if (w) { | ||
| const resolved = path.resolve(repoRoot, w); | ||
| // Prevent path traversal outside repoRoot | ||
| if (!resolved.startsWith(repoRoot + path.sep) && resolved !== repoRoot) { | ||
| return ''; | ||
| } | ||
| return resolved; | ||
| } | ||
| const candidates = [path.join(repoRoot, 'docs', 'wiki'), path.join(repoRoot, 'wiki')]; | ||
| for (const c of candidates) { | ||
| if (fs.existsSync(c)) return c; | ||
| } |
There was a problem hiding this comment.
resolveWikiDir only checks the lexical path prefix via startsWith, which can be bypassed by symlinks inside the repoRoot (e.g., --wiki docs/wikiLink pointing outside). Since generateRepoMap will read files from wikiDir, this can still allow reading outside the repo. Consider comparing fs.realpathSync(repoRoot) vs fs.realpathSync(resolved) (and/or using path.relative on realpaths) and returning an explicit error when the resolved realpath escapes repoRoot rather than silently returning ''.
| * Resolve wiki directory, ensuring the resolved path stays within repoRoot | |
| * to prevent path traversal attacks. | |
| */ | |
| export function resolveWikiDir(repoRoot: string, wikiOpt: string): string { | |
| const w = String(wikiOpt ?? '').trim(); | |
| if (w) { | |
| const resolved = path.resolve(repoRoot, w); | |
| // Prevent path traversal outside repoRoot | |
| if (!resolved.startsWith(repoRoot + path.sep) && resolved !== repoRoot) { | |
| return ''; | |
| } | |
| return resolved; | |
| } | |
| const candidates = [path.join(repoRoot, 'docs', 'wiki'), path.join(repoRoot, 'wiki')]; | |
| for (const c of candidates) { | |
| if (fs.existsSync(c)) return c; | |
| } | |
| * Check whether `candidate` is inside (or equal to) `root`, using a relative path | |
| * comparison. Both arguments must be absolute, normalized paths. | |
| */ | |
| function isPathInside(root: string, candidate: string): boolean { | |
| const rel = path.relative(root, candidate); | |
| if (rel === '') return true; | |
| return rel !== '..' && !rel.startsWith('..' + path.sep); | |
| } | |
| /** | |
| * Resolve wiki directory, ensuring the resolved path stays within repoRoot | |
| * (taking symlinks into account) to prevent path traversal attacks. | |
| */ | |
| export function resolveWikiDir(repoRoot: string, wikiOpt: string): string { | |
| const w = String(wikiOpt ?? '').trim(); | |
| const rootReal = fs.realpathSync(repoRoot); | |
| if (w) { | |
| const resolved = path.resolve(repoRoot, w); | |
| // If the target exists, validate using realpath to avoid symlink escapes. | |
| if (fs.existsSync(resolved)) { | |
| const resolvedReal = fs.realpathSync(resolved); | |
| if (!isPathInside(rootReal, resolvedReal)) { | |
| throw new Error(`Wiki directory must be inside the repository root: ${resolvedReal}`); | |
| } | |
| return resolvedReal; | |
| } | |
| // For non-existent paths, fall back to lexical containment check against the | |
| // realpath of repoRoot. This still prevents obvious escapes like "../../..". | |
| if (!isPathInside(rootReal, resolved)) { | |
| throw new Error(`Wiki directory must be inside the repository root: ${resolved}`); | |
| } | |
| return resolved; | |
| } | |
| const candidates = [path.join(repoRoot, 'docs', 'wiki'), path.join(repoRoot, 'wiki')]; | |
| for (const c of candidates) { | |
| if (!fs.existsSync(c)) continue; | |
| const cReal = fs.realpathSync(c); | |
| if (isPathInside(rootReal, cReal)) { | |
| return cReal; | |
| } | |
| } |
Summary
Fixes all 10 Copilot review comments from PR #22 (query-files command).
Changes
Security
resolveWikiDirnow rejects paths that escaperepoRoot(e.g.--wiki ../../...)Bug Fixes
globToSqlLike()to properly convert glob patterns (*→%,?→_) for SQL LIKE queries. Previouslysrc/*/handlerswould never match.query-filesin workspace mode sincequeryManifestWorkspacequeries by symbol, not file namelimit,maxCandidates,repoMapFiles,repoMapSymbols) are now properly parsed as integers before passing to handlerCode Quality
isCLIError,buildRepoMapAttachment,resolveWikiDir,inferLangFromFile,filterWorkspaceRowsByLang) intosharedHelpers.ts, used by bothqueryHandlers.tsandqueryFilesHandlers.tsresolveLangsfromqueryFilesHandlers.tsTest Improvements
SearchFilesSchema.parse()throws (Zod validation)SearchFilesSchema.parse()throws (Zod validation)resultin empty pattern testCloses review comments from #22